换一个帅一点姿势实现DexHunter
DexHunter 需要刷机,感觉太麻烦了,所以用xposed弄了一份。然后大部分解析逻辑都放到了java层,这样也有另一个好处,就是壳总不能修改java正常的api。所以 define class 之类的都可以直接调用 java 层的classloader 来初始化 class。
注意:目前我只在dalvik上面实现,art还没有看。
1. 寻找两个dalvik虚拟机函数,dvmDecodeIndirectRef 和dvmThreadSelf
/**
* 函数名称表根据4.4的Android版本设置的,不同Android版本映射可能存在差异,可以直接用ida查看维护
*/
void initDvmFunctionTables() {
void *libVMhandle = dlopen("libdvm.so", RTLD_GLOBAL | RTLD_LAZY);
initDvmFunctionItem("_Z20dvmDecodeIndirectRefP6ThreadP8_jobject",
(void **) (&dvmFunctionTables.dvmDecodeIndirectRef), libVMhandle);
initDvmFunctionItem("_Z13dvmThreadSelfv", (void **) (&dvmFunctionTables.dvmThreadSelf),
libVMhandle);
dlclose(libVMhandle);
}
2. 定位dex文件
其实我现在还不知道为啥dump内存需要那么麻烦,直接就能在内存中找到啊。看代码
extern "C"
JNIEXPORT jobject JNICALL
Java_com_virjar_xposedhooktool_unshell_Dumper_originDex(JNIEnv *env, jclass type,
jclass loader) {
//TODO check & throw exception
ClassObject *clazz = (ClassObject *) dvmFunctionTables.dvmDecodeIndirectRef(
dvmFunctionTables.dvmThreadSelf(),
loader);
DvmDex *dvm_dex = clazz->pDvmDex;
return env->NewDirectByteBuffer(dvm_dex->memMap.addr, dvm_dex->memMap.length);
}
这不就直接找到dex文件了嘛。
3. 创建dexFile模型,使用baksmaliAPI
private static DexBackedDexFile createMemoryDexFile(Class loader) {
ByteBuffer byteBuffer = originDex(loader);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
byte[] buffer = new byte[byteBuffer.capacity()];
byteBuffer.get(buffer, 0, byteBuffer.capacity());
if (HeaderItem.verifyMagic(buffer, 0)) {
return new DexBackedDexFile(Opcodes.forApi(apiLevel()), buffer);
//a normal dex file
}
if (OdexHeaderItem.verifyMagic(buffer, 0)) {
//this is a odex file
try {
ByteArrayInputStream is = new ByteArrayInputStream(buffer);
DexUtil.verifyOdexHeader(is);
is.reset();
byte[] odexBuf = new byte[OdexHeaderItem.ITEM_SIZE];
ByteStreams.readFully(is, odexBuf);
int dexOffset = OdexHeaderItem.getDexOffset(odexBuf);
if (dexOffset > OdexHeaderItem.ITEM_SIZE) {
ByteStreams.skipFully(is, dexOffset - OdexHeaderItem.ITEM_SIZE);
}
return new DexBackedOdexFile(Opcodes.forApi(Dumper.apiLevel()), odexBuf, ByteStreams.toByteArray(is));
} catch (IOException e) {
//while not happen
throw new RuntimeException(e);
}
}
throw new IllegalStateException("can not find out dex image in vm memory");
}
4. 使用smali APi rewrite功能,重构Method的指令数据
@Nonnull
@Override
public Method rewrite(@Nonnull final Method value) {
if (!(value instanceof DexBackedMethod)) {
return super.rewrite(value);
}
Class<?> definingClass;
try {
definingClass = classLoader.loadClass(value.getDefiningClass());
} catch (ClassNotFoundException e) {
return super.rewrite(value);
}
if (definingClass.getClassLoader() != classLoader) {
return super.rewrite(value);
}
final Class<?> searchClass = definingClass;
final String methodDescriptor = ReferenceUtil.getMethodDescriptor(value);
//覆盖 getImplementation
return new RewrittenMethod(value) {
@Nullable
@Override
public MethodImplementation getImplementation() {
ByteBuffer byteBuffer = methodDataWithDescriptor(methodDescriptor, value.getName(), searchClass);
if (byteBuffer == null) {
return super.getImplementation();
}
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
byte[] buffer = new byte[byteBuffer.capacity()];
byteBuffer.get(buffer, 0, byteBuffer.capacity());
DexBackedMethodImplementation dexBackedMethodImplementation = new DexBackedMethodImplementation(new MethodSegmentDexFile(buffer, dexFile), (DexBackedMethod) value, 0);
return rewriters.getMethodImplementationRewriter().rewrite(dexBackedMethodImplementation);
}
@Override
public int getAccessFlags() {
int accessFlags = getMethodAccessFlagsWithDescriptor(methodDescriptor, value.getName(), searchClass);
if (accessFlags < 0) {
//证明没有找到这个方法
return super.getAccessFlags();
}
return accessFlags;
}
};
}
5. 使用 dalvik dvmFindXXXMethodByDescriptor 功能
寻找对应的 method 对象,进而扣出真实的指令,这里指令长度计算,借用了 DexHunter 的代码
extern "C"
JNIEXPORT jobject JNICALL
Java_com_virjar_xposedhooktool_unshell_Dumper_methodDataWithDescriptor(JNIEnv *env, jclass type,
jstring methodDescriptor_,
jstring methodName_,
jclass searchClass) {
const char *methodDescriptor = env->GetStringUTFChars(methodDescriptor_, 0);
const char *methodName = env->GetStringUTFChars(methodName_, 0);
ClassObject *clazz = (ClassObject *) dvmFunctionTables.dvmDecodeIndirectRef(
dvmFunctionTables.dvmThreadSelf(),
searchClass);
jobject ret = NULL;
Method *method = dvmFindDirectMethodByDescriptor(clazz, methodName, methodDescriptor);
if (method == NULL) {
method = dvmFindVirtualMethodByDescriptor(clazz, methodName, methodDescriptor);
}
if (method == NULL) {
goto tail;
}
//check for native
uint32_t ac = (method->accessFlags) & accessFlagsMask;
if (method->insns == NULL || ac & ACC_NATIVE) {
goto tail;
}
//why 16
// 2 byte for registersSize
// 2 byte for insSize
// 2 byte for outsSize
// 2 byte for triesSize
// 4 byte for debugInfoOff
// 4 byte for insnsSize
// and then ,the insns address
DexCode *code = (DexCode *) ((const u1 *) method->insns - 16);
uint8_t *item = (uint8_t *) code;
int code_item_len = 0;
if (code->triesSize) {
const u1 *handler_data = dexGetCatchHandlerData(code);
const u1 **phandler = &handler_data;
uint8_t *tail = codeitem_end(phandler);
code_item_len = (int) (tail - item);
} else {
//正确的DexCode的大小
code_item_len = 16 + code->insnsSize * 2;
}
ret = env->NewDirectByteBuffer(item, code_item_len);
tail:
env->ReleaseStringUTFChars(methodDescriptor_, methodDescriptor);
env->ReleaseStringUTFChars(methodName_, methodName);
return ret;
}
6. binggo,使用smali api,直接解码
/**
* 将指定loader的smali全部dump到硬盘,请异步执行该函数
*
* @param loader loader
*/
public static void dissembleAllDex(Object loader) {
Class loaderClass = resolveLoaderClass(loader);
DexBackedDexFile memoryMethodDexFile = createMemoryDexFile(loaderClass);
File dumpDir = resolveDumpDir(memoryMethodDexFile);
DexFile reWritedDexFile = rewrite(memoryMethodDexFile, loaderClass.getClassLoader());
XposedBridge.log("脱壳目录:" + dumpDir.getAbsolutePath());
int jobs = Runtime.getRuntime().availableProcessors();
if (jobs > 6) {
jobs = 6;
}
if (memoryMethodDexFile instanceof DexBackedOdexFile) {
baksmaliOptions.inlineResolver = InlineMethodResolver
.createInlineMethodResolver(((DexBackedOdexFile) memoryMethodDexFile).getOdexVersion());
}
Log.i("weijia", "开始进行脱壳");
if (Baksmali.disassembleDexFile(reWritedDexFile, dumpDir, jobs, baksmaliOptions)) {
Log.i("weijia", "脱壳完成,但是存在错误");
} else {
Log.i("weijia", "脱壳成功,请在" + dumpDir + "中查看smali文件");
}
Toast.makeText(SharedObject.context, "脱壳完成,请在" + dumpDir + "中查看smali文件", Toast.LENGTH_LONG).show();
}
7.binggo,使用smali API,输出dex文件
/**
* 将对应class对应的dex文件的二进制dump出来
*
* @param loader 该dex文件定义的任何一个class,或者class定义的object
* @return 一个byteBuffer,包含了二进制数据
*/
public static ByteBuffer dumpDex(Object loader) {
Class loaderClass = resolveLoaderClass(loader);
DexBackedDexFile memoryDexFile = createMemoryDexFile(loaderClass);
byte[] buf = (byte[]) XposedHelpers.getObjectField(memoryDexFile, "buf");
DexFile dexFile = rewrite(memoryDexFile, loaderClass.getClassLoader());
final DexBuilder builder = new DexBuilder(Opcodes.forApi(apiLevel()));
MemoryDataStore memoryDataStore = new MemoryDataStore(buf.length);
for (ClassDef classDef : dexFile.getClasses()) {
try {
buildClassDef(classDef, builder);
} catch (Exception e) {
Log.i("weijia", "error when define class:" + classDef.getType() + " skipped for rebuild it");
}
}
try {
builder.writeTo(memoryDataStore);
} catch (IOException ioe) {
//the memory writer,no ioe happend
throw new RuntimeException(ioe);
}
return ByteBuffer.wrap(memoryDataStore.getData());
}
7. 如何调用
XposedHelpers.findAndHookConstructor(Activity.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
Object activity = param.thisObject;
if (activity == null) {
return;
}
XposedBridge.log("hook class " + activity.getClass());
if (StringUtils.equalsIgnoreCase(activity.getClass().getName(), "com.xxx.xxx.MainActivity")) {
Dumper.dumpDex(activity);
}
}
});
8. 对了你需要移植dalvik的dexlib模块代码,还有/vm/oo包下面的代码,到你的jni环境下。要不然没有dex的相关数据结构,也没有ClassObject的数据结构。
然后,不愿意放整个工程,不要求代码。论文放出来,还是自己实现一遍才能有收获。
其他:
相比原始DexHunter的优点:
1. 定位dex更加精准,DexHunter用过filename来确定当前dex是不是需要处理,很容易被加壳平台识别到这个特征。而且每次都要在放Android系统push一些文件,比较麻烦。我这个,直接通过class对象寻找,想找按个找那个。
2. 并发,可能没有写过Linux c语言程序,看那个pthread哪里看不懂。DexHunter为了防止多次重复处理同一个dex文件,写了比较复杂的加锁逻辑。这个放到java层,很简单实现吧。
3. 多dex,如果一个apk多个dex都需要处理。DexHunter不好处理,因为他输出就是whole.dex
4. dvmDefineClass失败,就算正常情况,一个classLoader下面的class,也不是全部可以正常load成功。DexHunter的流程是删除这些bad class,我还可以尽可能的使用原始dex数据进行解码。
5. Dalvik_dalvik_system_DexFile_defineClassNative被替换,这可能导致defineClassNative函数不被调用,这样DexHunter无法拦截到。我用classLoader.loadeClass,java标准接口,这个他永远没法替换。
6.其他数据结构加密调整,如果未来不光光是method的数据变化了。由于使用baksmali建立的模型,任何数据结构都很容易重构替换,毕竟是java,抽象封装很好用。
7.脱壳时机,脱壳输出控制更加方便。提供的是api,你调用就脱壳不调用就不脱壳。脱壳结果是java的二进制流,你想怎么编码、加密、转储都很方便。java层的api太多了。
8.可移植性,是一个普通的xposed项目,任何一个有xposed环境的Android机器都可以(当然现在还没有实现art,不过理论上没问题)。也不需要编译系统镜像。等几天再把xposed包装一下,免root脱壳。
当然,我对dex文件格式,并不是非常熟,反正没有DexHunter玩儿的那么溜,所以只有多用别人的api了。
本文由看雪论坛 virjar 原创
转载请注明来自看雪社区
往期热门阅读:
点击阅读原文/read,
更多干货等着你~
扫描二维码关注我们,更多干货等你来拿!